import parole, pygame

import random
import main, sim
from util import *

class Brain(object):
    waitTime = 1000

    def __init__(self, creature, stateMap):
        self.creature = creature
        self.stateMap = stateMap
        self.currentState = stateMap.startState
        main.schedule.add(sim.Event('think', main.schedule.time,
            (self.creature,), dests=[self.creature]))

    def listen(self, event):
        """
        Our host creature calls this for us on any events it receives.
        Handles:
            "think"    (creature)
        """
        if event.id == "think":
            creature = event.args[0]
            if creature is self.creature:

                # use our brain's statemap to determine what to do next
                transitions = self.stateMap.transitions
                for condition in transitions[self.currentState]:
                    conditionF = getattr(self, condition)
                    val = conditionF()
                    for transitionVal, availTransitions in \
                            transitions[self.currentState][condition].iteritems():
                        if val == transitionVal:
                            transition = chooseWeighted(availTransitions)
                            nextState, action = transition
                            self.currentState = nextState
                            actionF = getattr(self, action)
                            actionTime = actionF()
                            main.schedule.add(sim.Event('think', actionTime+1,
                                (self.creature,), dests=[self.creature]))
                            return

                # we didn't find anything to do. probably means a bug?
                parole.warn('***%r: no state transition conditions were met! '
                            'Thinking again in 1000ms.', self.creature)
                main.schedule.add(sim.Event('think', main.schedule.time+1000,
                    (self.creature,), dests=[self.creature]))


    def wait(self):
        #parole.debug('%r: waiting.', self.creature)
        return main.schedule.time + self.waitTime
        #main.schedule.add(sim.Event('think', main.schedule.time + self.waitTime,
        #    (self.creature,), dests=[self.creature]))

class StateMap(object):
    def __init__(self, transitions, startState):
        self.transitions = transitions
        self.startState = startState

# TODO: ideally, the player's "ai" should just be another brain that happens to
# listen to keyboard input to decide what to do. Would permit nifty effects like
# temporarily swapping the player's brain with another to achieve things like
# possession/lycanthropy, drunkenness/confusion, insanity, etc.

# general purpose statemap and brain that have the creature wait around until the
# player is "nearby", then pursues him, and flees if hp get too low.
# structure: (transitionmap, startstate)
# transitionmap: {srcstate : {condition : {val: [(transition, weight)] } } }
# transition: (deststate, method)
defaultStateMap = StateMap(
    {
        'wait-to-approach state': 
        {
            'wantToFleeLight':
            {
                True: [(('flee light state', 'fleeLight'), 10)]
            },

            'wantToFlee':
            {
                True: [(('wait-to-flee state', 'fleePlayer'), 10)]
            },

            'nearPlayer':
            {
                True:  [(('approach state', 'approachPlayer'), 10)],
                False: [(('wait-to-approach state', 'wait'), 10)]
            }
        },

        'approach state':
        {
            'wantToFleeLight':
            {
                True: [(('flee light state', 'fleeLight'), 10)]
            },

            'wantToFlee':
            {
                True: [(('flee state', 'fleePlayer'), 10)]
            },

            'nearPlayer':
            {
                True:  [(('approach state', 'approachPlayer'), 10)],
                False: [(('wait-to-approach state', 'wait'), 10)]
            }
        },

        'wait-to-flee state': 
        {
            'wantToFleeLight':
            {
                True: [(('flee light state', 'fleeLight'), 10)]
            },

            'wantToFlee':
            {
                False: [(('wait-to-approach state', 'fleePlayer'), 10)]
            },

            'nearPlayer':
            {
                True:  [(('flee state', 'fleePlayer'), 10)],
                False: [(('wait-to-flee state', 'wait'), 10)]
            }
        },

        'flee state':
        {
            'wantToFleeLight':
            {
                True: [(('flee light state', 'fleeLight'), 10)]
            },

            'wantToFlee':
            {
                False: [(('approach state', 'fleePlayer'), 10)]
            },

            'nearPlayer':
            {
                True:  [(('flee state', 'fleePlayer'), 10)],
                False: [(('wait-to-flee state', 'wait'), 10)]
            }
        },

        'flee light state':
        {
            'wantToFleeLight':
            {
                False: [(('wait-to-approach state', 'wait'), 10)],
                True: [(('flee light state', 'fleeLight'), 10)],
            }
        }
    },

    'wait-to-approach state' # start state
)

# TODO: extensible/pluggable system for different ways of directing creature
# movement. e.g., moveAStarPathTo(target), constrainedWander(radius)
#
# IDEA: constrainedWander: call 'north', 'south', 'east', 'west', etc. d_1, d_-1,
# d_2, d_-2, ..., d_4, d_-4; let r = radius; initially let each d_i = r; each
# turn, choose a direction d_i with prob ~ d_i, and set d_i -= 1 if i > 0 else
# d_-i += 1. Guaranteed never to wander more than r steps from origin (as the
# crow flies).

class DefaultMonsterBrain(Brain):
    """
    Default "fight or flight" brain. Works with defaultStateMap. Supports light
    tolerance and light sensitivity.
    """

    fleePercentage = 0.2 # % of hp
    fleeStamina = 'exhausted'

    def __init__(self, creature, stateMap=None, fleeHPPercentage=None,
            fleeStamina=None):
        super(DefaultMonsterBrain, self).__init__(creature, 
                stateMap or defaultStateMap)
        self.fleeing = False
        self.lastLightPenalty = None
        main.schedule.listen('enter tile', self)
        main.schedule.listen('leave tile', self)
        main.schedule.listen('activate light', self)
        main.schedule.listen('deactivate light', self)
        main.schedule.listen('update light', self)
        if fleeHPPercentage:
            self.fleePercentage = float(fleeHPPercentage)
        if fleeStamina:
            self.fleeStamina = fleeStamina

    def listen(self, event):
        #if self.creature['hp'] <= self.fleePercentage*self.creature['maxHP']:
        #    self.fleeing = True
        #else:
        #    self.fleeing = False
        super(DefaultMonsterBrain, self).listen(event)

        if event.id in ('enter tile', 'leave tile', 'activate light',
                'deactivate light', 'think', 'update light'):
            self.updateLightSensitivity()

    def updateLightSensitivity(self):
        creature = self.creature
        if not creature.lightSensitivity:
            return

        if creature.parentTile:
            penalty = creature.parentTile.lightIntensity * creature.lightSensitivity
        else:
            penalty = None

        if self.lastLightPenalty != penalty:
            if self.lastLightPenalty:
                main.schedule.sendNow(sim.Event('increase stat', main.schedule.time,
                    (creature, 'strengthMod', self.lastLightPenalty),
                    dests=[creature], options={'noMsg':1}))
                main.schedule.sendNow(sim.Event('increase stat', main.schedule.time,
                    (creature, 'dexterityMod', self.lastLightPenalty),
                    dests=[creature], options={'noMsg':1}))
                main.schedule.sendNow(sim.Event('increase stat', main.schedule.time,
                    (creature, 'speedMod', self.lastLightPenalty/4),
                    dests=[creature], options={'noMsg':1}))

            if penalty:
                main.schedule.sendNow(sim.Event('decrease stat', main.schedule.time,
                    (creature, 'strengthMod', penalty), dests=[creature],
                    options={'noMsg':1}))
                main.schedule.sendNow(sim.Event('decrease stat', main.schedule.time,
                    (creature, 'dexterityMod', penalty), dests=[creature],
                    options={'noMsg':1}))
                main.schedule.sendNow(sim.Event('decrease stat', main.schedule.time,
                    (creature, 'speedMod', penalty/4), dests=[creature],
                    options={'noMsg':1}))
        self.lastLightPenalty = penalty

    def nearPlayer(self):
        if not self.creature.parentTile or not main.player.parentTile:
            return False

        if self.creature.parentTile.map is not main.player.parentTile.map:
            return False

        # calculate how close the player has to be to be considered "near",
        # based on the creature's perception
        maxDistance = self.creature['perception']
        return distance(self.creature.pos, main.player.pos) <= maxDistance

    def wantToFlee(self):
        c = self.creature
        if self.fleeStamina == 'winded' and c.staminaLevel in ('winded', 'fatigued',
                'exhausted'):
            return True
        if self.fleeStamina == 'fatigued' and c.staminaLevel in ('fatigued',
                'exhausted'):
            return True
        if self.fleeStamina == 'exhausted' and c.staminaLevel == 'exhausted':
            return True

        # very simplistic flight mechanism for now -- if hp is below 15% of
        # maximum, reverse our direction, so as to flee rather than approach
        return (self.creature['hp'] <= self.fleePercentage*self.creature['maxHP'])

    def wantToFleeLight(self):
        return self.creature.lightTolerance > 0 and \
               (self.creature.parentTile.lightIntensity >= self.creature.lightTolerance)

    def moveOrRandom(self, dx, dy, time, transient=None):
        """
        Move self.creature in the indicated direction if possible, otherwise in
        a random direction. Returns True if a move was possible, or False if the
        creature is trapped.
        """
        dest = self.creature.pos[0] + dx, self.creature.pos[1] + dy
        neighbs = list(neighbors(self.creature.pos))
        random.shuffle(neighbs)

        map = self.creature.parentTile.map
        for d in [dest] + neighbs:
            if d[0] < 0 or d[1] < 0 or d[0] >= map.cols or d[1] >= map.rows:
                continue
            if not self.creature.blockedAt(map[d]):
                # destination is not blocked; schedule translocation thereto
                if self.wantToFleeLight() and map[d].lightIntensity > \
                        self.creature.parentTile.lightIntensity:
                    # ...unless it's too bright there
                    continue
                self.creature.doFatigue(self.creature.walkEnergy)
                main.schedule.add(sim.Event('translocate', time, 
                    args=(self.creature, d, map), dests=[self.creature],
                    options={'transient':transient}))
                return True
        return False

    def approachPlayer(self):
        parole.debug('%r: approaching player', self.creature)
        self.fleeing = False

        if main.player.parentTile in adjacentTiles(self.creature.parentTile):
            # We are already next to the player, so melee him
            return self.creature.melee(main.player)

        # move toward the player, if possible (no actual pathfinding yet, but
        # this is where it would go). otherwise, move randomly
        time = main.schedule.time + \
                self.creature.timeOfAction(self.creature.walkEnergy)
        diff = difference(main.player.pos, self.creature.pos)
        dx, dy = signVector(diff)

        if not self.moveOrRandom(dx, dy, time):
            parole.debug("%r: Egads, I'm trapped!", self.creature)

        return time

    def fleePlayer(self):
        parole.debug('%r: fleeing player', self.creature)
        if not self.fleeing:
            transient = '%s turns to flee!' % self.creature.noun(definite=1,
                    capitalized=1)
        else:
            transient = None
        self.fleeing = True

        # move away from the player, if possible (no actual pathfinding yet, but
        # this is where it would go). otherwise, move randomly
        time = main.schedule.time + \
                self.creature.timeOfAction(self.creature.walkEnergy)
        diff = difference(main.player.pos, self.creature.pos)
        dx, dy = signVector(diff)
        dx, dy = -dx, -dy

        if not self.moveOrRandom(dx, dy, time, transient):
            parole.debug("%r: Egads, I'm trapped!", self.creature)

        return time

    def fleeLight(self):
        parole.debug('%r: fleeing light', self.creature)
        if not self.fleeing:
            transient = '%s recoils from the light!' % self.creature.noun(definite=1,
                    capitalized=1)
        else:
            transient = None
        self.fleeing = True

        # Follow the light gradient
        map = self.creature.parentTile.map
        ptile = self.creature.parentTile
        time = main.schedule.time + \
                self.creature.timeOfAction(self.creature.walkEnergy)
        neighbs = list(neighbors(self.creature.pos))
        bestNeighb = None
        for n in neighbs:
            if (not bestNeighb) or map[n].lightIntensity <\
                    map[bestNeighb].lightIntensity:
                bestNeighb = n
        dx, dy = signVector(difference(main.player.pos, self.creature.pos))
        if not self.moveOrRandom(dx, dy, time, transient):
            parole.debug("%r: Egads, I'm trapped!", self.creature)

        return time


class PounceBrain(DefaultMonsterBrain):
    """Extends the default brain to allow attackers to make one "pounce" attack
    the first time they attack the player. This means they can jump over a
    certain number of squares to get next to the player and attack, as long as
    there are no intervening move-blockers, and the light is not too bright."""
    def __init__(self, creature, stateMap, jumpDist, jumpEnergy, jumpTime,
            jumpVerb="pounces on"):
        super(PounceBrain, self).__init__(creature, stateMap)
        self.jumpDist = jumpDist
        self.jumpEnergy = jumpEnergy
        self.jumpTime = jumpTime
        self.jumpVerb = jumpVerb
        self.alreadyPounced = False

    def approachPlayer(self):
        parole.debug('%r: PounceBrain.approachPlayer()', self.creature)
        if self.alreadyPounced:
            parole.debug('already pounced!')
            return super(PounceBrain, self).approachPlayer()
        if main.player.parentTile in adjacentTiles(self.creature.parentTile):
            parole.debug('already adjacent to player')
            return super(PounceBrain, self).approachPlayer()

        ppos = main.player.pos
        cpos = self.creature.pos
        map = self.creature.parentTile.map
        assert(map is main.player.parentTile.map)

        if distance(ppos, cpos) <= self.jumpDist:
            # is the player's pos too bright?
            if self.creature.lightTolerance and \
                    main.player.parentTile.lightIntensity >=\
                    self.creature.lightTolerance:
                parole.debug('player is too bright for pouncing')
                return super(PounceBrain, self).approachPlayer()

            # check for move blockers on the way to the player's pos
            #diff = difference(cpos, ppos)
            #parole.debug('cpos: %r, ppos: %r, diff: %r, dx: %r, dy: %r.' %\
            #        (cpos, ppos, diff, dx, dy))
            path = list(parole.map.bresenhamPoints(cpos, ppos))
            if path[0] != cpos:
                path.reverse()
            assert(path[0] == cpos and path[-1] == ppos)
            for p in path:
                if p == cpos:
                    continue
                if p == ppos:
                    break
                if self.creature.blockedAt(map[p]):
                    parole.debug('pounce path is blocked at: %r', map[p])
                    return super(PounceBrain, self).approachPlayer()

            parole.debug('%r: pouncing on the player.', self.creature)
            self.fleeing = False
            self.creature.doFatigue(self.jumpEnergy)
            main.schedule.add(sim.Event('translocate',
                main.schedule.time + self.jumpTime, 
                args=(self.creature, path[-2], map), dests=[self.creature],
                options={'transient':"%s %s you!" %
                    (self.creature.noun(definite=1, capitalized=1),
                        self.jumpVerb)}))
            self.alreadyPounced = True
            return main.schedule.time + self.jumpTime
        else:
            parole.debug('player is too far for pouncing')
            return super(PounceBrain, self).approachPlayer()


class TunnelBrain(DefaultMonsterBrain):
    def __init__(self, creature, stateMap, diggableObjClass=None,
            replacementObjClass=None, digVerb="digs through"):
        import dungeon
        super(TunnelBrain, self).__init__(creature, stateMap)
        self.diggableObjClass = diggableObjClass or dungeon.Rock
        self.replacementObjClass = replacementObjClass
        self.digVerb = digVerb

    def moveOrRandom(self, dx, dy, time, transient=None):
        dest = self.creature.pos[0] + dx, self.creature.pos[1] + dy
        map = self.creature.parentTile.map
        diggable = None
        for obj in map[dest]:
            if isinstance(obj, self.diggableObjClass) and not isinstance(obj,
                    self.replacementObjClass):
                diggable = obj
                continue
            elif obj.blocksMove:
                # something undiggable blocks our way
                return super(TunnelBrain, self).moveOrRandom(dx, dy, time,
                        transient=transient)

        if not diggable:
            # nothing blocks our way
            return super(TunnelBrain, self).moveOrRandom(dx, dy, time,
                    transient=transient)

        # okay, the only thing blocking our way is diggable, so dig it.
        parole.debug('%r: %s %s', self.creature, self.digVerb, diggable)
        dpos = diggable.pos
        map[dest].remove(diggable)
        if self.replacementObjClass:
            map[dest].add(self.replacementObjClass(diggable))
        map.update()
        if sim.visible(map[dest]):
            if sim.visible(self.creature):
                main.transient('%s %s %s.' % (self.creature.noun(definite=1,
                    capitalized=1), self.digVerb, diggable.noun(definite=1)),
                    dpos)
            else:
                main.transient('Something %s %s.' % (self.digVerb,
                    diggable.noun(definite=1)), dpos)

        return False

class ProjectileBrain(DefaultMonsterBrain):
    pass

class TelekinesisBrain(DefaultMonsterBrain):
    def __init__(self, creature, stateMap, telekinesisEnergy,
            telekinesisPush=False):
        super(TelekinesisBrain, self).__init__(creature, stateMap)
        self.telekinesisEnergy = telekinesisEnergy
        self.telekinesisPush = telekinesisPush

    def hasLOS(self, target):
        map = self.creature.parentTile.map
        if (not target.parentTile) or (target.parentTile.map is not map):
            return False
        return (not self.creature.dead) and (not target.dead) and\
                map.testLOS(self.creature.parentTile, target.parentTile)

    def approachPlayer(self):
        #parole.debug('********************* APPROACH')
        self.fleeing = False

        # see if we have LOS to the player
        map = self.creature.parentTile.map
        creature = self.creature
        if self.hasLOS(main.player):
            # we have LOS, so make our telekinetic attack
            time = main.schedule.time +\
                creature.timeOfAction(self.telekinesisEnergy)
            creature.doFatigue(self.telekinesisEnergy)

            if sim.visible(creature):
                main.transient("%s stares at you intently!" %
                        creature.noun(definite=1, capitalized=1),
                        creature.parentTile)

            # telekinetic attack automatically hits
            transient = "An invisible force slams you!"
            # draw damage
            damage = creature.drawUnarmedDamage(main.player)
            # schedule a "receive damage" event
            rcvDmg = sim.Event("receive damage", time, (main.player, creature,
                None, damage), dests=[main.player, creature],
                options={'transient':transient, 'type':'tele',
                    'confirm':self.hasLOS})
            main.schedule.add(rcvDmg)
            creature.pendingEvents.append(rcvDmg)
            return time
        else:
            # no LOS, so try to approach the player as normal
            return super(TelekinesisBrain, self).approachPlayer()


    def listen(self, event):
        super(TelekinesisBrain, self).listen(event)

        if event.id == 'receive damage':
            target, src, weap, dmg = event.args
            if src is not self.creature:
                return
            if self.creature.dead:
                return
            if not ('type' in event.options and event.options['type']=='tele'):
                return
            if not self.hasLOS(target):
                return
            creature = self.creature
            map = creature.parentTile.map
            if self.telekinesisPush:
                # push back
                ppos = main.player.pos
                cpos = creature.pos
                diff = difference(ppos, cpos)
                dx, dy = signVector(diff)
                newpos = (ppos[0] + dx, ppos[1] + dy)
                try:
                    newtile = map[newpos]
                except:
                    return 
                if target is main.player:
                    transient = "You are knocked back by the force of the "\
                                "invisible blow."
                else:
                    transient = "%s is knocked back by an invisible force!" %\
                            target.noun(definite=1, capitalized=1)
                if not main.player.blockedAt(newtile):
                    main.schedule.sendNow(sim.Event('translocate',
                        main.schedule.time, args=(target, newpos, map),
                        dests=[target], options={'transient':transient}))

                    # see if the target already has an upcoming translocate
                    # event scheduled, and cancel it
                    for e in main.schedule.eventHeap:
                        if e.id == 'translocate' and e.args[0] is target:
                            main.schedule.remove(e)
                            break

